消息组件:事件定义与样式优化
本节目标
- 定义完整的 TypeScript 接口(Props、Emits、ListItem、ActionItem)
- 将类型定义抽取到
types.d.ts统一管理 - 实现消息列表的事件机制(click-item、click-avatar、tab-click、action-click)
- 使用
computed过滤 Props 实现子组件属性透传
1. 类型定义提取
将所有接口统一放在 types.d.ts 中,方便多个组件共用。
1.1 完整类型文件
// components/notice/types.d.ts
import type { BadgeProps, AvatarProps, TagProps } from 'element-plus'
import type { IconifyProps } from '@iconify/vue'
/** 消息列表项 */
export interface MessageListItem {
/** 消息标题 */
title: string
/** 消息详细内容 */
content?: string
/** 消息时间 */
time?: string
/** 消息标签文字 */
tag?: string
/** 标签额外属性 */
tagProps?: Partial<TagProps>
/** 头像属性(继承 ElAvatar Props) */
avatar?: Partial<AvatarProps>
}
/** 操作按钮项 */
export interface NoticeActionItem {
/** 操作按钮文字 */
title: string
/** 操作按钮图标 */
icon?: string
/** 点击回调(必传) */
click: () => void
}
/** 消息列表配置(对应单个 Tab) */
export interface NoticeMessageListTab {
/** Tab 标题 */
title: string
/** Tab 下的消息列表 */
contents?: MessageListItem[]
}
/** NoticeMessageList 组件 Props */
export interface NoticeMessageListProps {
/** 消息列表(按 Tab 分组) */
list: NoticeMessageListTab[]
/** 底部操作按钮 */
actions: NoticeActionItem[]
/** 外层容器 class */
wrapClass?: string
/** 外层容器 style */
wrapStyle?: import('vue').CSSProperties
}
/** 基础通知组件 Props */
export interface NotificationProps extends Partial<BadgeProps> {
/** 徽章图标 */
icon?: string
/** 徽章背景颜色 */
color?: string
/** 徽章字号 */
size?: number
/** 图标颜色 */
iconColor?: string
/** 图标大小 */
iconSize?: number
/** 缩放比例 */
scale?: number
}
/** Notice 组合组件 Props(聚合所有子组件 Props) */
export interface NoticeProps
extends NoticeMessageListProps,
Partial<NotificationProps> {}
ts
1.2 设计思路
为什么把 Props 分层?
用户使用 Notice 组件 → 传入 NoticeProps
↓
Notice 内部分发
├── list + actions → NoticeMessageList
└── icon + color + size + scale → Notification
text
将子组件的常用 Props 全部提升到 NoticeProps,用户只需面对一个 Props 接口。Notice 内部用 computed 过滤后分别传递给各子组件。
Partial<TagProps> vs TagProps:列表项中的 tagProps 和 avatar 都是可选的部分属性,用 Partial 包裹避免 TypeScript 报必选字段缺失。
2. NoticeMessageList 事件定义
2.1 Emits 类型
// 使用 defineEmits 的 TypeScript 类型声明方式
const emit = defineEmits<{
(e: 'click-avatar', avatar: Partial<AvatarProps>): void
(e: 'click-item', item: MessageListItem): void
(e: 'click-tab', context: TabsPaneContext, event: Event): void
}>()
ts
2.2 事件触发
import type { TabsPaneContext } from 'element-plus'
// 点击消息项
const handleClickItem = (item: MessageListItem) => {
emit('click-item', item)
}
// 点击头像
const handleClickAvatar = (item: MessageListItem) => {
emit('click-avatar', item.avatar || {})
}
// 切换标签页
const handleTabClick = (context: TabsPaneContext, event: Event) => {
emit('click-tab', context, event)
}
ts
2.3 事件与 Props 的区别
| 场景 | 用 Props 传递 | 用 Emits 触发 |
|---|---|---|
| 底部 Action 按钮 | 不确定数量和内容,通过 actions 数组动态渲染 | 每项自带 click 回调 |
| 消息项点击 | 固定事件 | emit('click-item') |
| 头像点击 | 固定事件 | emit('click-avatar') |
| Tab 切换 | 固定事件 | emit('click-tab') |
3. Notice 组合组件 Props 过滤
3.1 computed 过滤方案
// components/notice/Notice.vue
const props = defineProps<NoticeProps>()
// 过滤出 Notification 需要的 props(排除 list 和 actions)
const filteredProps = computed(() => {
const { list, actions, ...rest } = props
// 消除 unused 警告
void list
void actions
return rest
})
ts
3.2 事件透传方案
<template>
<el-dropdown trigger="click" :hide-on-click="false">
<Notification v-bind="filteredProps" />
<template #dropdown>
<el-dropdown-item :disabled="true" class="!p-0">
<NoticeMessageList
:list="list"
:actions="actions"
wrap-class="w-300px"
@click-item="forwardedEvents.onClickItem"
@click-avatar="forwardedEvents.onClickAvatar"
@click-tab="forwardedEvents.onClickTab"
/>
</el-dropdown-item>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import Notification from './Notification.vue'
import NoticeMessageList from './NoticeMessageList.vue'
import type { NoticeProps } from './types'
import type { MessageListItem } from './types'
import type { AvatarProps, TabsPaneContext } from 'element-plus'
const props = defineProps<NoticeProps>()
const emit = defineEmits<{
(e: 'click-item', item: MessageListItem): void
(e: 'click-avatar', avatar: Partial<AvatarProps>): void
(e: 'click-tab', context: TabsPaneContext, event: Event): void
}>()
// 事件转发对象
const forwardedEvents = {
onClickItem: (item: MessageListItem) => emit('click-item', item),
onClickAvatar: (avatar: Partial<AvatarProps>) => emit('click-avatar', avatar),
onClickTab: (context: TabsPaneContext, event: Event) => emit('click-tab', context, event),
}
const filteredProps = computed(() => {
const { list, actions, ...rest } = props
void list
void actions
return rest
})
</script>
vue
4. 模板中的数据绑定
4.1 外层 Tab 遍历
<el-tab-pane
v-for="(tab, tabIndex) in list"
:key="tabIndex"
:label="tab.title"
:name="tab.title"
>
html
4.2 内层消息遍历
<ul v-if="tab.contents && tab.contents.length > 0" class="message-list">
<li
v-for="(item, index) in tab.contents"
:key="index"
@click="handleClickItem(item)"
>
<!-- 头像 -->
<el-avatar
v-if="item.avatar"
v-bind="Object.assign({ size: 'small' }, item.avatar)"
@click.stop="handleClickAvatar(item)"
/>
<!-- 标题 + 标签 -->
<div class="message-title line-clamp-1">{{ item.title }}</div>
<el-tag v-if="item.tag" v-bind="item.tagProps" size="small" effect="dark">
{{ item.tag }}
</el-tag>
<!-- 内容 -->
<div v-if="item.content" class="message-desc line-clamp-2">
{{ item.content }}
</div>
<!-- 时间 -->
<div v-if="item.time" class="message-time">{{ item.time }}</div>
</li>
</ul>
html
注意:
- 头像的
@click.stop阻止事件冒泡,避免同时触发li的click-item v-if条件判断确保数据不存在时不渲染对应 DOM
5. 关键知识点总结
| 知识点 | 说明 |
|---|---|
types.d.ts | 集中管理类型定义,多组件共用 |
interface extends | 组合多个 Props 接口,减少重复定义 |
defineEmits<{}> | TypeScript 泛型方式声明事件类型 |
computed 过滤 Props | 解构排除不需要的属性,剩余透传 |
@click.stop | 阻止事件冒泡,区分头像点击与消息点击 |
Partial<T> | 将类型所有属性标记为可选 |
Object.assign | 合并默认属性与用户传入属性 |
6. 下一节预告
下一节是更新节,将介绍 Vue 3 中 CSS v-bind() 指令的使用,替代 CSS 变量的方式实现更简洁的响应式样式绑定。
↑